﻿using System;
using Microsoft.VisualStudio.Modeling;
using System.Collections.Generic;
using System.Globalization;

namespace Project.Co_DataTransformationPrototype
{
    #region NamedElement Change Rule
    /// <summary>
    /// Defines a change rule for Element name changes.
    /// </summary>
    [RuleOn(typeof(NamedElement), FireTime = TimeToFire.TopLevelCommit)]
    public class ElementNameUniquenessChangeRule : ChangeRule
    {
        /// <summary>
        /// Alerts listeners that a property for an element has changed.
        /// </summary>
        /// <param name="e">Provides data for the ElementPropertyChanged event.</param>
        public override void ElementPropertyChanged(ElementPropertyChangedEventArgs e)
        {
            bool alreadyHandled = false;

            // Validate arguments
            if (e == null)
                throw new ArgumentNullException("e");

            // Property being changed
            if (e.DomainProperty.Id.Equals(NamedElement.NameDomainPropertyId))
                alreadyHandled = HandledNameChange(e);

            if (alreadyHandled)
            {
                // Show message as exception to void triggering of event recording
                string message = string.Format(CultureInfo.CurrentCulture, "Cannot change {0}'s name from '{1}' to '{2}'. Another element in the model has this same name. The change will be cancelled.", e.ModelElement.GetDomainClass().DisplayName, e.OldValue.ToString(), e.NewValue.ToString());
                throw new ModelingException(message);
            }

            // Default behavior
            base.ElementPropertyChanged(e);
        }

        /// <summary>
        /// Checks costum rule for changes to the Name property.
        /// </summary>
        /// <param name="e">The <see cref="Microsoft.VisualStudio.Modeling.ElementPropertyChangedEventArgs"/> instance containing the event data.</param>
        private bool HandledNameChange(ElementPropertyChangedEventArgs e)
        {
            bool blnHandled = false;
            
            if (!e.ModelElement.Store.InSerializationTransaction)
            {
                string oldName = (string)e.OldValue;
                string newName = (string)e.NewValue;

                if ((string.IsNullOrEmpty(oldName) && !string.IsNullOrEmpty(newName)) ||
                    (!newName.Equals(oldName, StringComparison.CurrentCulture)))
                {
                    if ((e.ModelElement as Entity) != null)
                        blnHandled = RuleHelper.PreventRepeatedName<Entity>(e.ModelElement as Entity);
                    else
                        blnHandled = RuleHelper.PreventRepeatedName<EntityAttribute>(e.ModelElement as EntityAttribute);
                }
            }
            return blnHandled;
        }
    }
    #endregion

    #region EntityAttribute Add Rule
    /// <summary>
    /// Defines a add rule for the attribute creation
    /// </summary>
    [RuleOn(typeof(EntityAttribute), FireTime = TimeToFire.LocalCommit)]
    public class AttributeNameUniquenessAddRule : AddRule
    {
        /// <summary>
        /// Alerts listeners that an attribute was added
        /// </summary>
        /// <param name="e">Provides data for the ElementAddedEventArgs event.</param>
        public override void ElementAdded(ElementAddedEventArgs e)
        {
            bool alreadyHandled = false;

            // Validate arguments
            if (e == null)
                throw new ArgumentNullException("e");

            // Attribute being created
            alreadyHandled = HandledNameCheck(e);

            if (alreadyHandled)
            {
                EntityAttribute movingAttribute = e.ModelElement as EntityAttribute;
                Entity parentEntity = movingAttribute.Entity;

                if (parentEntity == null)
                    parentEntity = movingAttribute.ParentEntity;
                
                if (parentEntity != null)
                {
                    using (Transaction t = e.ModelElement.Store.TransactionManager.BeginTransaction("Change attribute Name"))
                    {
                        EntityAttribute attribute = parentEntity.EntityAttributes.Find(obj => obj.Name == movingAttribute.Name);

                        if (attribute != null)
                        {
                            throw new ModelingException(string.Format("Cannot create {0} with name '{1}'. Another element in the entity has this same name.", e.ModelElement.GetDomainClass().DisplayName, movingAttribute.Name));
                        }
                    }
                }
                else
                {
                    throw new NotImplementedException("EntityNameUniqueness validation error - could not retrieve entity from transaction context.");
                }
            }
            base.ElementAdded(e);
        }

        /// <summary>
        /// Checks costum rule for the Name property.
        /// </summary>
        /// <param name="e">The <see cref="Microsoft.VisualStudio.Modeling.ElementPropertyChangedEventArgs"/> instance containing the event data.</param>
        private bool HandledNameCheck(ElementAddedEventArgs e)
        {
            bool blnHandled = false;

            if (!e.ModelElement.Store.InSerializationTransaction)
            {
                EntityAttribute attribute = e.ModelElement as EntityAttribute;
                Entity parentEntity = attribute.Entity;

                blnHandled = RuleHelper.PreventRepeatedName<EntityAttribute>(attribute, false);
            }
            return blnHandled;
        }
    }
    #endregion


    #region Register Rule
    public partial class Co_DataTransformationPrototypeDomainModel
    {
        protected override Type[] GetCustomDomainModelTypes()
        {
            List<Type> types = new List<Type>(base.GetCustomDomainModelTypes());
            types.Add(typeof(ElementNameUniquenessChangeRule));
            types.Add(typeof(AttributeNameUniquenessAddRule));
            // If you add more rules, list them here. 
            return types.ToArray();
        }
    }
    #endregion

    #region Rule Helper class
    public static class RuleHelper
    {
        /// <summary>
        /// Checks if the specified model element has any sibling with the same name. If so it rolls back
        /// the current transaction and shows a message to the user.
        /// </summary>
        /// <param name="modelElement">The model element whose name will be checked.</param>
        internal static bool PreventRepeatedName<T>(T modelElement, bool doRollBack = true)
            where T : NamedElement
        {
            bool blnPrevented = false;

            // Validation
            if (modelElement == null)
                throw new ArgumentNullException("Model element passed to 'PreventRepeatedName' cannot be null");

            ICollection<T> eCollection = modelElement.Store.ElementDirectory.FindElements<T>(true);// GetSiblings
            if (eCollection != null)
            {
                // Search any element that has the same name
                foreach (T element in eCollection)
                {
                    if (!element.Id.Equals(modelElement.Id) && element.Name.Equals(modelElement.Name, StringComparison.CurrentCulture))
                    {
                        // If it's an attribute, then check if both attributes are in the same entity
                        if ((element as EntityAttribute) != null)
                        {
                            Entity parent;
                            // Get entity parent
                            if (modelElement.Store.InUndoRedoOrRollback)
                                parent = (modelElement as EntityAttribute).ParentEntity;
                            else
                                parent = (modelElement as EntityAttribute).Entity;

                            // If there not in the same entity, skip name validation
                            if (!parent.Name.Equals((element as EntityAttribute).Entity.Name))
                                continue;
                        }

                        // Rollback transaction to cancel change
                        if (doRollBack)
                            modelElement.Store.TransactionManager.CurrentTransaction.Rollback();

                        blnPrevented = true;
                        break;
                    }
                }
            }
            return blnPrevented;
        }

    }
    #endregion
}